﻿/**
 * author:  Martin Hrabos
 * year:    2016
 * email:   hrabosmartin@gmail.com
 * 
 * FIT Brno University of Technology / ASCR Brno
 */

using Cloo;
using ManagedCuda;
using ManagedCuda.BasicTypes;
using ManagedCuda.VectorTypes;
using signalViewer;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace CorrCL
{
    /*
     * Class related to computations on GPU
     */
    class GpuComputation : Computation
    {

        // OpenCL associated members
        private ComputeContext Context;
        private ComputeProgram prog;
        private ComputeCommandQueue Queue;
        private int maxWorkGroupSize;

        // Cuda associated members
        private CudaKernel fillVectorWithCuda;
        long compUnits;

        public GpuComputation(int pid)
        {
            platformID = pid;
        }

        // Run required computation
        public override void compute()
        {
            if (cudaSelected)
                computeCuda();
            else
                computeOpenCL();
        }

        public void computeCuda()
        {
            
            CudaContext Context = new CudaContext(CudaContext.GetMaxGflopsDeviceId());
            CUmodule module = Context.LoadModule(@"plugins/pearson.ptx");

            CudaDeviceProperties props = Context.GetDeviceInfo();

            fillVectorWithCuda = new CudaKernel("_Z7PearsonPfS_S_lll", module, Context);

            compUnits = props.MaxGridDim.x;

            // Define threads and blocks
            dim3 threads = new dim3(props.MaxThreadsPerBlock, 1);
            dim3 blocks = new dim3((uint)(compUnits / props.MaxThreadsPerBlock), 1);

            fillVectorWithCuda.BlockDimensions = threads;
            fillVectorWithCuda.GridDimensions = blocks;

            // Define result size
            long workItems = 2 * offset + 1;
            int progress = 0;

            // Get number of computations
            int computationCount = ((data.Count * data.Count) - data.Count) / 2;
            int processedComputations = 0;
            float[] result = new float[workItems];

            OnProgressChanged(0, "Executing");
            Stopwatch sw = new Stopwatch();
            sw.Start();

            for (int i = 0; i < data.Count - 1; i++)
            {
                for (int j = i + 1; j < data.Count; j++)
                {

                    long startIdx = 0;
                    long endIdx = 0;
                    List<float[]> blockList = new List<float[]>();
                    do
                    {
                        endIdx = startIdx + windowSize - 1;
                        if (endIdx > size)
                            endIdx = size;
                        long currentWindowSize = endIdx - startIdx;
                        float[] xData = new float[currentWindowSize];
                        float[] yData = new float[currentWindowSize];
                        Array.Copy(data[i].ToArray(), startIdx, xData, 0, currentWindowSize);
                        Array.Copy(data[j].ToArray(), startIdx, yData, 0, currentWindowSize);

                        float[] item = computeBlockCUDA(startIdx, endIdx, xData, yData, offset);
                        blockList.Add((float[])item.Clone());

                        startIdx += windowOffset;
                        Debug.WriteLine(startIdx);
                    }

                    while (endIdx < size - 1);


                    // Add result to ResultMatrix
                    rm.Add(new List<float[]>(blockList), j - 1, i);


                    processedComputations++;
                    progress = (int)((processedComputations / (float)computationCount) * 100);
                    OnProgressChanged(progress, String.Format("Processed {0} %", progress));

                }
            }

            OnProgressChanged(100, "Finished");

            sw.Stop();

            TimeSpan t = sw.Elapsed;
            string answer = string.Format("{0:D2}:{1:D2}.{2:D3}",
                                    t.Minutes,
                                    t.Seconds,
                                    t.Milliseconds);


            OnProgressChanged(100, "Total Time: " + answer);
        }



        private float[] computeBlockCUDA(long startIdx, long endIdx, float[] xData, float[] yData, long offset)
        {
            float[] result = new float[2 * offset + 1];
            long windowSize = endIdx - startIdx;

            // Create GPU variables
            CudaDeviceVariable<float> x = new CudaDeviceVariable<float>(xData.Length);
            CudaDeviceVariable<float> y = new CudaDeviceVariable<float>(yData.Length);
            CudaDeviceVariable<float> res = new CudaDeviceVariable<float>(2 * offset + 1);

            // Copy memory from RAM to GPU
            x.CopyToDevice(xData);
            y.CopyToDevice(yData);

            long processed = 0;
            int step = 0;
            do
            {
                // Run kernel
                fillVectorWithCuda.Run(x.DevicePointer, y.DevicePointer, res.DevicePointer, offset, windowSize, processed);

                processed += compUnits;
                step++;

            }
            while (processed < 2*offset+1);

            // Copy results from GPU to RAM
            res.CopyToHost(result);

            x.Dispose();
            y.Dispose();
            res.Dispose();

            return result;
        }

        private float[] computeBlockCL(long startIdx, long endIdx, float[] xData, float[] yData, long offset)
        {
            float[] result = new float[2 * offset + 1];
            long windowSize = endIdx - startIdx;

            // Create GPU variables
            ComputeBuffer<float> xBuff = new ComputeBuffer<float>(Context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, xData);
            ComputeBuffer<float> yBuff = new ComputeBuffer<float>(Context, ComputeMemoryFlags.ReadOnly | ComputeMemoryFlags.CopyHostPointer, yData);
            ComputeBuffer<float> resBuff = new ComputeBuffer<float>(Context, ComputeMemoryFlags.WriteOnly | ComputeMemoryFlags.CopyHostPointer, result);

            IntPtr mappedX = Queue.Map(xBuff, true, ComputeMemoryMappingFlags.Read, 0, windowSize, null);
            IntPtr mappedY = Queue.Map(yBuff, true, ComputeMemoryMappingFlags.Read, 0, windowSize, null);
            IntPtr mappedRes = Queue.Map(resBuff, true, ComputeMemoryMappingFlags.Write, 0, 2*offset+1, null);

            ComputeKernel kernel = prog.CreateKernel("Pearson");

            long processed = 0;
            int step = 0;
            do
            {
                // Setting arguments for kernel function
                kernel.SetMemoryArgument(0, xBuff);
                kernel.SetMemoryArgument(1, yBuff);
                kernel.SetMemoryArgument(2, resBuff);
                kernel.SetValueArgument(3, offset);
                kernel.SetValueArgument(4, windowSize);
                kernel.SetValueArgument(5, processed);
                Queue.Execute(kernel, null, new long[] { maxWorkGroupSize }, null, null);
                processed += maxWorkGroupSize;
                step++;
            }

            while (processed < 2*offset+1);


            // Wait for completition
            Queue.Finish();
            // Read data from GPU
            Queue.ReadFromBuffer(resBuff, ref result, true, null);
            // Wait for reading data
            Queue.Finish();

            Queue.Unmap(xBuff, ref mappedX, null);
            Queue.Unmap(yBuff, ref mappedY, null);
            Queue.Unmap(resBuff, ref mappedRes, null);

            

            xBuff.Dispose();
            yBuff.Dispose();
            resBuff.Dispose();

            return result;
        }

        public void computeOpenCL()
        {
            // Define result size
            long workItems = 2 * offset + 1;

            OnProgressChanged(0, "Initializing device");
            ComputeContextPropertyList Properties = new ComputeContextPropertyList(ComputePlatform.Platforms[platformID]);


            Context = new ComputeContext(ComputeDeviceTypes.All, Properties, null, IntPtr.Zero);

            // Retrieve source code
            string pearsonSrc = KernelSources.getPearsonSources();

            List<ComputeDevice> Devs = new List<ComputeDevice>();
            Devs.Add(ComputePlatform.Platforms[platformID].Devices[0]);

            
            float[] result = new float[workItems];
            try
            {
                // Build GPU program
                prog = new ComputeProgram(Context, pearsonSrc);
                prog.Build(Devs, "", null, IntPtr.Zero);
                //prog.Build(null, null, null, IntPtr.Zero);
            }

            catch (Exception exp)
            {
                mainView.log(exp, "Compiling OpenCL kernel", this);
            }

            // Create command queue
            Queue = new ComputeCommandQueue(Context, ComputePlatform.Platforms[platformID].Devices[0], ComputeCommandQueueFlags.None);
            // Get max number of preocessing units for selected platform
            maxWorkGroupSize = (int)ComputePlatform.Platforms[platformID].Devices[0].MaxWorkGroupSize;

            //Debug.WriteLine("Processes: " + maxWorkGroupSize);

            // Get memory restrictions 
            long globMemSize = ComputePlatform.Platforms[platformID].Devices[0].GlobalMemorySize;
            long maxMemAllocSize = ComputePlatform.Platforms[platformID].Devices[0].MaxMemoryAllocationSize;
            //Debug.WriteLine("Max Global memory: " + globMemSize / 1024 / 1024);
            //Debug.WriteLine("Max Alloc memory: " + maxMemAllocSize / 1024 / 1024);

            // Get number of computations
            int computationCount = ((data.Count * data.Count) - data.Count) / 2;
            int processedComputations = 0;

            // Creating kernels
            var kernels = new List<ComputeKernel>();

            OnProgressChanged(0, "Executing");
            Stopwatch sw = new Stopwatch();
            sw.Start();
            int progress = 0;

            for (int i = 0; i < data.Count - 1; i++)
            {
                for (int j = i + 1; j < data.Count; j++)
                {

                    long startIdx = 0;
                    long endIdx = 0;
                    List<float[]> blockList = new List<float[]>();
                    do
                    {
                        endIdx = startIdx + windowSize - 1;
                        if (endIdx > size)
                            endIdx = size;
                        long currentWindowSize = endIdx - startIdx;
                        float[] xData = new float[currentWindowSize];
                        float[] yData = new float[currentWindowSize];
                        Array.Copy(data[i].ToArray(), startIdx, xData, 0, currentWindowSize);
                        Array.Copy(data[j].ToArray(), startIdx, yData, 0, currentWindowSize);

                        float[] item = computeBlockCL(startIdx, endIdx, xData, yData, offset);
                        blockList.Add((float[])item.Clone());

                        startIdx += windowOffset;
                        Debug.WriteLine(startIdx);
                    }

                    while (endIdx < size - 1);


                    // Add result to ResultMatrix
                    rm.Add(new List<float[]>(blockList), j - 1, i);

                    result.Initialize();

                    processedComputations++;
                    progress = (int)((processedComputations / (float)computationCount) * 100);
                    OnProgressChanged(progress, String.Format("Processed {0} %", progress));
                }
            }



            OnProgressChanged(100, "Finished");

            sw.Stop();

            TimeSpan t = sw.Elapsed;
            string answer = string.Format("{0:D2}:{1:D2}.{2:D3}",
                                    t.Minutes,
                                    t.Seconds,
                                    t.Milliseconds);


            OnProgressChanged(100, "Total Time: " + answer);


            Debug.WriteLine(prog);


            prog.Dispose();
            Queue.Dispose();
            Context.Dispose();


        }
    }
}
